#include <gtest/gtest.h>

#include <thread>
#include <variant>

#include "bridge/NativeToJSBridge.h"
#include "jsi_executor/JSIExecutor.h"
#include "JSIExecutorTest.h"
#include "test_bridge/MyMessageQueue.h"
#include "test_native/MyAsyncModule.h"
#include "test_native/MyNativeModule.h"

TEST_P(JSIExecutorTest, SyncNativeCall) {
  testing::internal::CaptureStdout();
  auto myQueue = std::make_shared<test_bridge::MyMessageQueue>("JsQueue");
  std::vector<std::unique_ptr<jsiexecutor::NativeModule>> nativeModules;
  auto myNativeModule = std::make_unique<test_native_module::MyNativeModule>(1);
  nativeModules.emplace_back(std::move(myNativeModule));

  bridge::NativeToJSBridge nativeToJSBridge(
      std::move(runtime), std::make_shared<jsiexecutor::ModuleRegistry>(std::move(nativeModules)), myQueue);

  std::thread producer1([&nativeToJSBridge]() { nativeToJSBridge.initializeRuntime(); });

  std::thread producer2([&nativeToJSBridge]() {
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::vector<Dynamic> params;
    params.push_back(Dynamic((int64_t)1));
    params.push_back(Dynamic((int64_t)3));
    nativeToJSBridge.callFunction("myNativeModule", "testFunc", params);
  });

  std::thread producer3([&nativeToJSBridge]() {
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::vector<Dynamic> params;
    params.push_back(Dynamic((int64_t)3));
    params.push_back(Dynamic((int64_t)5));
    nativeToJSBridge.callFunction("myNativeModule", "testMax", params);
  });

  std::thread consumer([&myQueue]() {
    for (int i = 0; i < 3; i++) {
      myQueue->execute();
    }
  });

  producer1.join();
  producer2.join();
  producer3.join();
  consumer.join();

  std::string output = testing::internal::GetCapturedStdout();

  int msgCountToJsQueue = 0;
  for (size_t i = 0; (i = output.find("submitted msg to JsQueue", i)) != std::string::npos;) {
    msgCountToJsQueue++;
    i++;
  }
  EXPECT_EQ(msgCountToJsQueue, 3);

  EXPECT_NE(output.find("testMax called with param: x: 3, y: 5"), std::string::npos);
  EXPECT_NE(output.find("testFunc called with param x: 1, y:3"), std::string::npos);
}

TEST_P(JSIExecutorTest, ASyncNativeCall) {
  testing::internal::CaptureStdout();
  auto jsQueue = std::make_shared<test_bridge::MyMessageQueue>("JsQueue");

  std::vector<std::unique_ptr<jsiexecutor::NativeModule>> nativeModules;
  auto moduleRegistry = std::make_shared<jsiexecutor::ModuleRegistry>(std::move(nativeModules));

  auto nativeToJSBridge = std::make_shared<bridge::NativeToJSBridge>(std::move(runtime), moduleRegistry, jsQueue);

  std::vector<std::unique_ptr<jsiexecutor::NativeModule>> modulesToBeAdded;

  auto nativeQueue = std::make_shared<test_bridge::MyMessageQueue>("NativeQueue");
  auto myNativeModule = std::make_unique<test_native_module::MyAsyncModule>(nativeToJSBridge, nativeQueue);

  modulesToBeAdded.emplace_back(std::move(myNativeModule));
  moduleRegistry->registerModules(std::move(modulesToBeAdded));

  std::thread producer1([&nativeToJSBridge]() { nativeToJSBridge->initializeRuntime(); });

  std::thread producer2([&nativeToJSBridge]() {
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::string script = R"(
    const NativeModules = globalThis.NativeModules;
    
    NativeModules["MyAsyncModule"]["testMax"](3, 15,
        ([x, y]) => { console.log("callback invoked with params x: " + x + ", y:" + y); },
    );
    console.log("invokeAsync req submitted but not be processed yet");
  )";

    nativeToJSBridge->loadBundle(std::move(script), "test");
  });

  std::thread jsConsumer([&jsQueue]() {
    // two producers above and one more for callbacks created by native module
    for (int i = 0; i < 3; i++) {
      jsQueue->execute();
    }
  });

  std::thread nativeConsumer([&nativeQueue]() {
    std::this_thread::sleep_for(std::chrono::milliseconds(30));
    nativeQueue->execute();
  });

  producer1.join();
  producer2.join();
  jsConsumer.join();
  nativeConsumer.join();

  std::string output = testing::internal::GetCapturedStdout();

  int msgCountToJsQueue = countOccurrences(output, "submitted msg to JsQueue");
  EXPECT_EQ(msgCountToJsQueue, 3);

  int msgCountToNativeQueue = countOccurrences(output, "submitted msg to NativeQueue");
  EXPECT_EQ(msgCountToNativeQueue, 1);

  EXPECT_NE(output.find("testMax called with param: x: 3, y: 15"), std::string::npos);
  EXPECT_NE(output.find("callback invoked with params x: 3, y:15"), std::string::npos);
}